﻿namespace Library.Navigation
{
    using System;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Ink;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    using System.Collections.Generic;
    using System.Text;
    using System.Linq;
    using Phone = System.Windows.Navigation;
    using Microsoft.Phone.Shell;
    using Library.Storage;

    public class NavigationProvider
    {
        private static NavigationProvider instance = new NavigationProvider();

        public static NavigationProvider Instance
        {
            get
            {
                return instance;
            }
        }

        private const string UniqueParameterName = "navigation_provider_page_index";

        private Frame frame;

        private NavigationEntry currentEntry;

        private NavigationEntry targetEntry;

        private Dictionary<object, NavigationMapItem> sourceToPagePathMapping = new Dictionary<object, NavigationMapItem>();

        private Dictionary<string, NavigationMapItem> pagePathToSourceMapping = new Dictionary<string, NavigationMapItem>();

        private List<NavigationEntry> navigationStack = new List<NavigationEntry>();

        private bool isNavigationExecutingNow;

        private bool needRestoreStack = true;

        public NavigationProvider()
        {
            this.currentEntry = new NavigationEntry 
			{
				IsUnknown = true
			};

            navigationStack.Add(this.currentEntry);
            this.targetEntry = currentEntry;
            this.NavigationStackSerializer = new NavigationHistoryStorageProvider();
        }

        public INavigationHistoryStorageProvider NavigationStackSerializer { get; set; }

        public event NavigationProviderEventHandler Navigated;

        public event NavigationProviderCancellableEventHandler Navigating;

        public event NavigationProviderEventHandler NavigationCancelled;

        

        public void Initialize(Frame frame, NavigationMapper mapper, PhoneApplicationService applicationService)
        {
            foreach (var mapItem in mapper.Mappings)
            {
                this.sourceToPagePathMapping.Add(mapItem.Source, mapItem);
                this.pagePathToSourceMapping.Add(mapItem.PagePath, mapItem);
            }

            this.frame = frame;
            this.frame.Navigating += this.OnInternalFrameNavigating;
            this.frame.Navigated += OnInternalFrameNavigated;
            this.frame.NavigationStopped += OnInternalFrameNavigationStopped;
            this.frame.NavigationFailed += OnInternalFrameNavigationFailed;

            applicationService.Launching += this.OnApplicationLaunching;
            applicationService.Activated += this.OnApplicationActivated;
            applicationService.Deactivated += this.OnApplicationDeactivated;
        }

        public void Navigate(object source)
        {
            this.Navigate(source, null);
        }

        public void Navigate(object source, NavigationParameter parameter)
        {
            if (!this.isNavigationExecutingNow)
            {
                var uri = this.BuildUri(source, parameter);
                var navigationEntry = new NavigationEntry();
                navigationEntry.Source = source;
                navigationEntry.Parameter = parameter;
                this.targetEntry = navigationEntry;
                this.frame.Navigate(uri);
            }
        }

        private void OnInternalFrameNavigating(object sender, Phone.NavigatingCancelEventArgs e)
        {
            this.isNavigationExecutingNow = true;
            var navigationMode = NavigationModeConverter.ConvertFromSystem(e.NavigationMode);
            NavigationEntry fromEntry = null;
            fromEntry = this.navigationStack[this.navigationStack.Count - 1];

            if (navigationMode == NavigationMode.New)
            {
                bool isProviderNavigation = this.targetEntry != this.currentEntry;
                if (!isProviderNavigation)
                {
                    this.targetEntry = this.CreateNavigationEntry(e.Uri);
                }
            }
            else
            {
                this.targetEntry = this.navigationStack[this.navigationStack.Count - 2];
            }
            
            var handler = this.Navigating;
            if (handler != null)
            {
                var navigatingEventArgs = new NavigationProviderCancellableEventArgs(fromEntry, this.targetEntry, navigationMode);
                handler.Invoke(this, navigatingEventArgs);
                bool isCancelled = navigatingEventArgs.IsCancelled;
                if (isCancelled)
                {
                    e.Cancel = true;
                }
            }
        }

        private void OnInternalFrameNavigationStopped(object sender, Phone.NavigationEventArgs e)
        {
            this.isNavigationExecutingNow = false;
            var mode = NavigationModeConverter.ConvertFromSystem(e.NavigationMode);
            var handler = this.NavigationCancelled;
            if (handler != null)
            {
                var args = new NavigationProviderEventArgs(this.currentEntry, this.targetEntry, mode);
                handler.Invoke(this, args);
            }

            this.targetEntry = this.currentEntry;
        }

        private void OnInternalFrameNavigationFailed(object sender, Phone.NavigationFailedEventArgs e)
        {
            this.isNavigationExecutingNow = false;
            this.targetEntry = this.currentEntry;
        }

        private void OnInternalFrameNavigated(object sender, Phone.NavigationEventArgs e)
        {
            this.isNavigationExecutingNow = false;
            var previousEntity = this.currentEntry;
            this.currentEntry = this.targetEntry;
            var navigationMode = NavigationModeConverter.ConvertFromSystem(e.NavigationMode);
            if (navigationMode == NavigationMode.New)
            {
                this.navigationStack.Add(this.targetEntry);
            }
            else if (navigationMode == NavigationMode.Back)
            {
                this.navigationStack.RemoveAt(this.navigationStack.Count - 1);
            }

            var handler = this.Navigated;
            if (handler != null)
            {
                var args = new NavigationProviderEventArgs(previousEntity, this.currentEntry, navigationMode);
                handler.Invoke(this, args);
            }
        }

        private Uri BuildUri(object source, NavigationParameter parameter)
        {
            NavigationMapItem mappingItem = null;
            bool mappingFounded = this.sourceToPagePathMapping.TryGetValue(source, out mappingItem);
            if (!mappingFounded)
            {
                var exceptionMessage = String.Format("Source {0} not mapped to any page", source.ToString());
                throw new ArgumentException(exceptionMessage, "source");
            }

            string pagePath = mappingItem.PagePath;
            var uriParameters = new Dictionary<string, string>();
            if (parameter != null && parameter.AllowStoreToUri)
            {
                uriParameters = parameter.StoreToUriParameters();
            }

            uriParameters[UniqueParameterName] = Guid.NewGuid().ToString();

            var parametersAdditionalSymbol = "?";
            if (pagePath.Contains("?"))
            {
                parametersAdditionalSymbol = "&";
            }

            var uriBuilder = new StringBuilder(pagePath + parametersAdditionalSymbol);

            foreach (var item in uriParameters)
            {
                uriBuilder.Append(item.Key);
                uriBuilder.Append("=");
                var escapedValue = Uri.EscapeDataString(item.Value);
                uriBuilder.Append(escapedValue);
                uriBuilder.Append("&");
            }

            var uri = new Uri(uriBuilder.ToString(), UriKind.Relative);
            return uri;
        }

        private NavigationEntry CreateNavigationEntry(Uri uri)
        {
            var navigationEntry = new NavigationEntry();
            navigationEntry.IsUnknown = this.IsExternalNavigation(uri);
            if (!navigationEntry.IsUnknown)
            {
                var uriSource = uri.OriginalString;
                string pagePath = uriSource;
                string parametersPart = String.Empty;
                var indexOfParameterSeparator = uriSource.IndexOf('?');
                if (indexOfParameterSeparator != -1)
                {
                    pagePath = uriSource.Substring(0, indexOfParameterSeparator);
                    var parametersStringStartIndex = indexOfParameterSeparator + 1;
                    if (parametersStringStartIndex < uriSource.Length)
                    {
                        parametersPart = uriSource.Substring(parametersStringStartIndex, uriSource.Length - parametersStringStartIndex);
                    }
                }

                NavigationMapItem mapping = null;
                bool mappingFounded = this.pagePathToSourceMapping.TryGetValue(pagePath, out mapping);
                if (mappingFounded)
                {
                    navigationEntry.Source = mapping.Source;
                    if (mapping.ParameterType != null)
                    {
                        var parameter = (NavigationParameter)Activator.CreateInstance(mapping.ParameterType);
                        if (parameter.AllowInitializingFromUri)
                        {
                            var parameters = new Dictionary<string, string>();
                            var parametersKeyValuesString = parametersPart.Split('&');
                            foreach (var keyValueString in parametersKeyValuesString)
                            {
                                var keyValuePair = keyValueString.Split('=');
                                string key = keyValuePair[0];
                                string value = String.Empty;
                                if (keyValuePair.Length > 1)
                                {
                                    value = keyValuePair[1];
                                }

                                parameters[key] = value;
                            }

                            parameter.InitializeFromUriParameters(parameters);
                        }

                        navigationEntry.Parameter = parameter;
                    }
                }
                else
                {
                    // Frame can open pages that was not mapped to any source. This can be third-party controls. 
                    navigationEntry.IsUnknown = true;
                }
            }

            return navigationEntry;
        }

        private bool IsExternalNavigation(Uri uri)
        {
            return uri.OriginalString == "app://external/";
        }

        private void OnApplicationLaunching(object sender, LaunchingEventArgs e)
        {
        }

        private void OnApplicationActivated(object sender, ActivatedEventArgs e)
        {
            if (this.needRestoreStack)
            {
                this.navigationStack = this.NavigationStackSerializer.RestoreHistory();
                bool isSuccessfullyRestored = this.navigationStack.Count > 0;
                if (isSuccessfullyRestored)
                {
                    this.currentEntry = this.navigationStack[this.navigationStack.Count - 1];
                    this.targetEntry = this.currentEntry;
                }
                else
                {
                    //TODO: navigate to default page.
                }
            }
        }

        private void OnApplicationDeactivated(object sender, DeactivatedEventArgs e)
        {
            this.needRestoreStack = false;
            this.NavigationStackSerializer.StoreHistory(this.navigationStack);
        }
    }
}
